﻿namespace LaoLuo
{
    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Text.RegularExpressions;

    /// <summary>
    /// Provides static method to render string with template and data
    /// </summary>
    public class TextTemplate
    {
        /// <summary>
        /// an instance of <see cref="TextTemplateCache"/>
        /// </summary>
        private static TextTemplateCache _cache = new TextTemplateCache();

        /// <summary>
        /// close tag in <see cref="_template"/>
        /// </summary>
        private string _closeTag;

        /// <summary>
        /// open tag in <see cref="_template"/>
        /// </summary>
        private string _openTag;

        /// <summary>
        /// template to be used in rendering
        /// </summary>
        private string _template;

        /// <summary>
        /// formatted template(can be used in string.Format) to be used in rendering
        /// </summary>
        private string _templateFormated;

        /// <summary>
        /// ordered keys in <see cref="_template"/>
        /// </summary>
        private List<string> _orderedKeys;

        /// <summary>
        /// ctor
        /// </summary>
        /// <param name="template">template to be used in rendering</param>
        /// <param name="openTag">open tag in <paramref name="template"/></param>
        /// <param name="closeTag">close tag in <paramref name="template"/></param>
        private TextTemplate(string template, string openTag, string closeTag)
        {
            ThrowHelper.ThrowIfNullOrEmpty(template);
            ThrowHelper.ThrowIfNullOrWhiteSpace(openTag, closeTag);

            this._template = template;
            this._openTag = openTag;
            this._closeTag = closeTag;

            ParseTemplate();
        }

        /// <summary>
        /// Gets a rendered string using specified params
        /// </summary>
        /// <typeparam name="T">type of <paramref name="dataSource"/></typeparam>
        /// <param name="template">template to render</param>
        /// <param name="dataSource">entity or IDictionary&lt;string, object> is supported</param>
        /// <param name="openTag">open tag in <paramref name="template"/></param>
        /// <param name="closeTag">close tag in <paramref name="template"/></param>
        /// <param name="ignoreCase">if true, properties' names in entity or keys in dictionary will be case-ignored when rendering</param>
        /// <returns>rendered string</returns>
        public static string Render<T>(string template, T dataSource, string openTag = "[%", string closeTag = "%]", bool ignoreCase = true)
        {
            if (dataSource is IDictionary<string, object>)
            {
                return _cache[template, openTag, closeTag].RenderDictionary(dataSource as IDictionary<string, object>, ignoreCase);
            }

            return _cache[template, openTag, closeTag].RenderEntity(dataSource, ignoreCase);
        }

        /// <summary>
        /// format template to make it can be used in string.Format
        /// </summary>
        private void ParseTemplate()
        {
            _orderedKeys = new List<string>();
            _templateFormated = Regex.Replace(
                _template,
@"(?<open>" + Regex.Escape(this._openTag) + @")(?<key>\w+)(?<format>:[^}]+)?(?<close>" + Regex.Escape(this._closeTag) + ")",
                (m) =>
                {
                    string key = m.Groups["key"].Value; ////.ToLowerInvariant()
                    string format = m.Groups["format"].Value;

                    if (!_orderedKeys.Contains(key))
                    {
                        _orderedKeys.Add(key);
                    }

                    return string.Format("{{{0}{1}}}", _orderedKeys.IndexOf(key), format);
                });
        }

        /// <summary>
        /// Gets a rendered string
        /// </summary>
        /// <param name="values">data used in rendering</param>
        /// <param name="ignoreCase">if true, keys in dictionary will be case-ignored when rendering</param>
        /// <returns>rendered string</returns>
        private string RenderDictionary(IDictionary<string, object> values, bool ignoreCase = true)
        {
            var orderedValues = new object[_orderedKeys.Count];

            for (int i = 0; i < _orderedKeys.Count; i++)
            {
                var k = values.Keys.FirstOrDefault(j => j.EqualsOrdinal(_orderedKeys[i], ignoreCase));
                orderedValues[i] = k != null ? values[k] : string.Empty;
            }

            return string.Format(_templateFormated, orderedValues);
        }

        /// <summary>
        /// Gets a rendered string
        /// </summary>
        /// <typeparam name="T">type of <paramref name="entity"/></typeparam>
        /// <param name="entity">data used in rendering</param>
        /// <param name="ignoreCase">if true, properties' names in entity will be case-ignored when rendering</param>
        /// <returns>rendered string</returns>
        private string RenderEntity<T>(T entity, bool ignoreCase = true)
        {
            return RenderDictionary(entity.ToDictionary(_orderedKeys, ignoreCase));
        }

        /// <summary>
        /// cache container of <see cref="TextTemplate"/>
        /// </summary>
        private class TextTemplateCache
        {
            /// <summary>
            /// cache of <see cref="TextTemplate"/>
            /// </summary>
            private ConcurrentDictionary<string, TextTemplate> _cache = new ConcurrentDictionary<string, TextTemplate>();

            /// <summary>
            /// Gets a <see cref="TextTemplate"/>
            /// </summary>
            /// <param name="template">template to be used in rendering</param>
            /// <param name="openTag">open tag in <paramref name="template"/></param>
            /// <param name="closeTag">close tag in <paramref name="template"/></param>
            /// <returns>a <see cref="TextTemplate"/></returns>
            public TextTemplate this[string template, string openTag, string closeTag]
            {
                get
                {
                    return _cache.GetOrAdd(template, (key) => new TextTemplate(template, openTag, closeTag));
                }
            }
        }
    }
}
